package compile;

import common.FileUtil;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * @program: java_oj
 * @author: JCM
 * @create: 2023-08-31 09:09
 * @description: 每次 编译+运行 的过程就是一个 compile.Task
 **/
public class Task {
    private  String  WORK_DIR = "./tmp/";
    // 约定代码类名
    private String CLASS = null;
    // 约定要编译的代码文件名
    private String CODE = null;
    // 存放编译错误信息的文件名
    private String COMPILE_ERROR = null;
    // 存放运行时的标准输出的文件名
    private String STDOUT = null;
    // 存放运行时的标准错误的文件名
    private String STDERR = null;

    public Task() {
        // 保证生成唯一的路径
        WORK_DIR = "./tmp/"+ UUID.randomUUID().toString() + "/";
        CLASS = "Solution";
        CODE = WORK_DIR+"Solution.java";
        COMPILE_ERROR = WORK_DIR + "compileError.txt";
        STDOUT = WORK_DIR + "stdout.txt";
        STDERR = WORK_DIR + "stderr.txt";
    }


    // 参数：要编译运行的 java 源代码
    // 返回值：表示编译运行的结果，编译出错/运行出错/运行正确...
    public Answer compileAndRun(Question question) {
        Answer answer = new Answer();
        // 准备好用来存放临时文件的目录
        File workDir = new File(WORK_DIR);
        if (!workDir.exists()) {
            // 创建多级目录
            workDir.mkdirs();
        }
        // 进行安全判定
        if(!checkCodeSafe(question.getCode())) {
            System.out.println("用户提交了不安全的代码！");
            answer.setError(3);
            answer.setReason("您提交的代码不符合要求，禁止运行！");
            return answer;
        }
        // 1. 把 question 中的code 写入到一个 Solution.java 文件中
        FileUtil.writeFile(CODE,question.getCode());
        // 2. 创建子进程，调用 javac 进行编译.注意：编译的时候需要有一个.java 文件
        //      如果编译出错，javac 就会把错误信息写入到 stderr 里，保存在 compileError.txt 中
        String compileCmd = String.format("javac -encoding utf8 %s -d %s",CODE,WORK_DIR);
        System.out.println(compileCmd);
        CommandUtil.run(compileCmd,null,COMPILE_ERROR);
        // 如果编译出错，错误信息就会记录到 COMPILE_ERROR 文件中
        String compileError = FileUtil.readFile(COMPILE_ERROR);
        if (!compileError.equals("")) {
            // 直接返回 compile.Answer，让 compile.Answer 记录编译的错误信息
            System.out.println("编译出错！");
            answer.setError(1);
            answer.setReason(compileError);
            return answer;
        }

        // 3. 创建子进程，调用 java 命令并执行
        //      运行程序时，也会把 java 子进程的标准输出和标准错误获取到，保存到 stdout.txt,stderr.txt 中
        String runCmd = String.format("java -classpath %s %s",WORK_DIR,CLASS);
        System.out.println("运行命令："+runCmd);
        CommandUtil.run(runCmd,STDOUT,STDERR);
        String runError = FileUtil.readFile(STDERR);
        if(!runError.equals("")) {
            System.out.println("运行出错！");
            answer.setError(2);
            answer.setReason(runError);
            return answer;
        }
        // 4. 父进程获取到刚才的编译执行的结果，并打包成 compile.Answer 对象
        //      编译执行的结果，通过约定的文件获取即可
        answer.setError(0);
        answer.setStdout(FileUtil.readFile(STDOUT));
        return answer;
    }

    private boolean checkCodeSafe(String code) {
        List<String> blackList = new ArrayList<>();
        // 防止提交的代码运行恶意程序
        blackList.add("Runtime");
        blackList.add("exec");
        // 防止提交的代码读写文件
        blackList.add("java.io");
        // 防止提交的代码访问网络
        blackList.add("java.net");
        for (String target: blackList) {
            int pos = code.indexOf(target);
            if (pos >= 0) {
                // 找到恶意代码特征
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Task task = new Task();
        Question question = new Question();
        question.setCode("public class Solution {\n" +
                "    public static void main(String[] args) {\n" +
                "        System.out.println(\"hello world\");\n" +
                "    }\n" +
                "}");
        Answer answer = task.compileAndRun(question);
        System.out.println(answer);
    }
}
